Skip to content

Feat: searchable project selector#3068

Merged
HarshMN2345 merged 6 commits into
mainfrom
feat-ser542-searchable-project-selector
May 29, 2026
Merged

Feat: searchable project selector#3068
HarshMN2345 merged 6 commits into
mainfrom
feat-ser542-searchable-project-selector

Conversation

@HarshMN2345
Copy link
Copy Markdown
Member

What does this PR do?

(Provide a description of what this PR does.)

Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)

Related PRs and Issues

(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)

Have you read the Contributing Guidelines on issues?

(Write your answer here.)

HarshMN2345 and others added 2 commits May 29, 2026 14:41
The project selector previously loaded up to 100 projects upfront,
silently truncating orgs with more. Replace InputSelect with
Input.ComboBox per row — each row fetches the first 25 projects on
open, then debounces API search as the user types. Already-selected
projects are filtered from each row's results. Removes the orgProjects
prefetch from createMember entirely; edit modal no longer needs a
projects prop.
Two root causes of the shift were identified and fixed:

1. Melt UI's `usePopper` applies `position: absolute` to the dropdown
   via floating-ui after a `tick()` delay. During that one frame the
   `ul` was briefly in normal document flow, momentarily growing the
   `<dialog>` element and causing a reflow. Pre-setting
   `[data-melt-combobox-menu] { position: absolute }` in base.css
   removes the element from flow immediately at mount.

2. Melt UI's ComboBox has `preventScroll: true` by default. On open it
   calls `removeScroll()` which sets `body.overflow: hidden` and adds
   compensatory `padding-right` for the scrollbar width — a body
   reflow that shifts the dialog. Pre-setting `data-melt-scroll-lock`
   on the body in `modal.svelte` tells Melt UI the lock is already
   active so it returns early without touching the body.

Also fix `on:search` on `Input.ComboBox` (which the component never
dispatches) by replacing it with native `oninput`/`onfocusin` handlers
on the wrapper `<div>`, so typed text actually triggers debounced
server-side project search. Projects are now ordered newest-first
(`Query.orderDesc('')`) matching the org projects page.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR replaces the static project list prop on ProjectAccessSelector with per-row async search (debounced, with stale-response guards via generation counters and a shared prefetch promise), and wires in a scroll-lock workaround for Melt UI ComboBox inside modals.

  • projectAccessSelector.svelte: Removes the projects prop; each row now fetches its own paginated results with debounced search, generation-counter race protection, and sibling-cache invalidation on project selection.
  • modal.svelte + base.css: Adds a reactive data-melt-scroll-lock attribute mirror and a global CSS rule to pre-position combobox menus absolutely, fixing layout shift and scroll interference.
  • Caller cleanup (createMember.svelte, edit.svelte, +page.svelte): Removes the parent-side project fetch and prop-passing now that the child is self-sufficient.

Confidence Score: 4/5

Safe to merge with minor follow-up; the async search logic is well-guarded but a few rough edges remain in the combobox UX.

The generation-counter race fix and sibling-cache invalidation are solid improvements. The main outstanding concerns are the removed Add project disabled guard (users can add rows that can never be filled), open threads around empty-search feedback and the modal scroll-lock conflict with nested modals, and a mixed Svelte 4/5 event-syntax inconsistency. None of these are data-loss issues, but the UX gaps and the open threads from the previous review round suggest the component deserves another pass before merging.

src/routes/(console)/organization-[organization]/projectAccessSelector.svelte and src/lib/components/modal.svelte warrant the closest look.

Important Files Changed

Filename Overview
src/routes/(console)/organization-[organization]/projectAccessSelector.svelte Major refactor: removed the projects prop and replaced it with per-row async search (debounced, with generation counters and prefetch). Several previously-flagged issues remain open (empty search results give no feedback; debouncers for rows after a removed row leak stale closures). New issue: "Add project" button has no upper bound after removal of the allSelected guard.
src/lib/components/modal.svelte Adds a reactive block that mirrors scroll-lock state via a data attribute on body; the unconditional removeAttribute on hide still conflicts with nested modals (previously flagged, not addressed).
src/lib/profiles/css/base.css Adds a global rule to pre-set combobox menus as absolutely positioned, preventing a one-frame layout shift before Melt UI's floating-ui repositioning kicks in.
src/routes/(console)/organization-[organization]/createMember.svelte Removes the local orgProjects fetch and passes ProjectAccessSelector without a projects prop now that the child fetches its own data.
src/routes/(console)/organization-[organization]/members/+page.svelte Stops passing orgProjects to the Edit component; a one-line cleanup matching the new self-fetching architecture.
src/routes/(console)/organization-[organization]/members/edit.svelte Drops the projects prop and passes ProjectAccessSelector without it; straightforward cleanup.

Reviews (3): Last reviewed commit: "perf: prefetch project list on component..." | Re-trigger Greptile

Comment thread src/routes/(console)/organization-[organization]/projectAccessSelector.svelte Outdated
Comment thread src/routes/(console)/organization-[organization]/projectAccessSelector.svelte Outdated
Comment thread src/lib/components/modal.svelte
When a project was selected in one row, other rows' cached option lists
still included it, allowing duplicate project assignments. Two places
needed cache busting:

- `onProjectSelected(i)`: clears options for all rows except the one
  that just made a selection; they reload fresh (with takenIds applied)
  on next focus.
- `removeRow(i)`: clears all sibling caches after removal so the
  freed-up project reappears in other rows' dropdowns on next open.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Two bugs in the project access selector:

1. Race condition: loadProjects had no cancellation guard. Two in-flight
   requests for the same row (e.g. rapid typing) could resolve out of
   order, leaving stale results visible. Added a per-row generation
   counter — responses are discarded if a newer request has been
   dispatched since they were started.

2. Edit-mode UUIDs: when the edit modal opens with a member's existing
   project-specific roles, rowOptions starts empty so Input.ComboBox
   falls back to displaying the raw projectId. A $effect now eagerly
   calls loadProjects for any row that has a projectId but no loaded
   options, resolving the label as soon as the dropdown mounts.
…delay

The project list was only fetched after a row appeared, causing a
~1 second blank dropdown. Now the API call fires the instant the
ProjectAccessSelector mounts (when the user switches to "Specific
projects"), so data is ready or in-flight before any interaction.

All rows share a single Promise for unfiltered loads via prefetchPromise;
typed searches still hit the API individually. The generation-counter
race guard still applies so concurrent awaits can't overwrite each other.
@HarshMN2345 HarshMN2345 merged commit 80beffe into main May 29, 2026
4 checks passed
@HarshMN2345 HarshMN2345 deleted the feat-ser542-searchable-project-selector branch May 29, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants